package com.gitee.usl.grammar.utils;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.IdentityHashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;

import com.gitee.usl.grammar.ScriptEngine;
import com.gitee.usl.grammar.runtime.type._Null;
import com.gitee.usl.grammar.script.Script;
import com.gitee.usl.grammar.Feature;
import com.gitee.usl.grammar.runtime.function.FunctionUtils;
import com.gitee.usl.grammar.runtime.function.internal.ReducerResult;

public class Env implements Map<String, Object> {

    /**
     * Default values map.
     */
    private final Map<String, Object> mDefaults;

    /**
     * Current evaluator instance that executes current expression.
     */
    private transient ScriptEngine instance;

    /**
     * Override values map.
     */
    private Map<String, Object> mOverrides;

    private Script expression;

    private List<String> importedSymbols;

    private List<String> importedPackages;

    // Caching resolved classes
    private transient Map<String/* class name */, Class<?>> resolvedClasses;

    public static final Map<String, Object> EMPTY_ENV = Collections.emptyMap();

    /**
     * Constructs an env instance with empty state.
     */
    public Env() {
        this(EMPTY_ENV);
    }

    /**
     * Constructor.
     *
     * @param defaults map providing defaults for keys not set directly
     */
    public Env(final Map<String, Object> defaults) {
        this.mDefaults = defaults;
    }

    public Env(final Map<String, Object> defaults, final Map<String, Object> overrides) {
        this.mDefaults = defaults;
        this.mOverrides = overrides;
    }

    public void setmOverrides(final Map<String, Object> mOverrides) {
        this.mOverrides = mOverrides;
    }

    public List<String> getImportedSymbols() {
        return this.importedSymbols;
    }

    public Script getExpression() {
        return this.expression;
    }

    public void setExpression(final Script expression) {
        this.expression = expression;
    }

    public Map<String, Object> getDefaults() {
        return this.mDefaults;
    }

    public String addSymbol(final String sym) {
        if (this.importedSymbols == null) {
            this.importedSymbols = new ArrayList<>();
        }
        this.importedSymbols.add(sym);
        invalidateCache();
        return sym;
    }

    public String addPackageSymbol(final String sym) {
        if (this.importedPackages == null) {
            this.importedPackages = new ArrayList<>();
        }
        this.importedPackages.add(sym);
        invalidateCache();
        return sym;
    }

    private void invalidateCache() {
        if (this.resolvedClasses != null) {
            this.resolvedClasses.clear();
        }
    }

    public ScriptEngine getInstance() {
        return this.instance;
    }

    public void setInstance(final ScriptEngine instance) {
        this.instance = instance;
    }

    public void configure(final ScriptEngine instance, final Script exp) {
        this.instance = instance;
        this.expression = exp;
    }

    private String findSymbol(final String name) throws ClassNotFoundException {
        final String postfix = "." + name;
        String targetSym = null;
        if (this.importedSymbols != null) {
            for (String sym : this.importedSymbols) {
                if (sym.endsWith(postfix)) {
                    targetSym = sym;
                    break;
                }
            }
        }
        return targetSym;
    }

    public Class<?> resolveClassSymbol(final String name) throws ClassNotFoundException {
        return this.resolveClassSymbol(name, true);
    }

    public Class<?> resolveClassSymbol(final String name, final boolean checkIfAllow)
            throws ClassNotFoundException {
        // from cache
        Class<?> clazz = retrieveFromCache(name);
        if (clazz == NullClass.class) {
            throw new ClassNotFoundException(name);
        }
        if (clazz == null) {
            if (name.contains(".")) {
                clazz = classForName(name);
            } else {
                // java.lang.XXX
                clazz = classForName("java.lang." + name);
                // from imported packages
                if (clazz == null) {
                    clazz = resolveFromImportedPackages(name);
                }
                // from imported classes
                if (clazz == null) {
                    clazz = resolveFromImportedSymbols(name, clazz);
                }
                // try to find from parent env.
                if (clazz == null && this.mDefaults instanceof Env) {
                    clazz = ((Env) this.mDefaults).resolveClassSymbol(name, checkIfAllow);
                }
            }
        }

        if (clazz == null) {
            put2cache(name, NullClass.class);
            throw new ClassNotFoundException(name);
        }
        put2cache(name, clazz);
        return clazz;
    }

    private Class<?> resolveFromImportedPackages(final String name) {
        Class<?> clazz = null;
        if (this.importedPackages != null) {
            for (String pkg : this.importedPackages) {
                clazz = classForName(pkg + name);
                if (clazz != null) {
                    return clazz;
                }
            }
        }
        return clazz;
    }

    private Class<?> resolveFromImportedSymbols(final String name, Class<?> clazz)
            throws ClassNotFoundException {
        final String classSym = findSymbol(name);
        if (classSym != null) {
            clazz = classForName(classSym);
        }
        return clazz;
    }

    private void put2cache(final String name, final Class<?> ret) {
        if (this.resolvedClasses == null) {
            this.resolvedClasses = new HashMap<>();
        }
        this.resolvedClasses.put(name, ret);
    }

    private Class<?> retrieveFromCache(final String name) {
        return this.resolvedClasses != null ? this.resolvedClasses.get(name) : null;
    }

    private Class<?> classForName(final String name) {
        try {
            return Class.forName(name);
        } catch (ClassNotFoundException e) {
            return null;
        }
    }

    /**
     * Clear all override key-value pairs. This only effects the overrides, not the defaults.
     */
    @Override
    public void clear() {
        if (this.mDefaults != EMPTY_ENV) {
            this.mDefaults.clear();
        }
        if (this.mOverrides != null && this.mOverrides != EMPTY_ENV) {
            this.mOverrides.clear();
        }
    }

    /**
     * Check if a key has a defined value. This will return <code>true</code> if the key is present in
     * the overrides map or the defaults map.
     *
     * @param key
     * @return <code>true</code> if key defined, <code>false</code> if not
     */
    @Override
    public boolean containsKey(final Object key) {
        Map<String, Object> overrides = getmOverrides(true);
        return overrides.containsKey(key)
                || (this.mDefaults != overrides ? this.mDefaults.containsKey(key) : false);
    }

    /**
     * Check if a value is present.
     *
     * @param value
     * @return <code>true</code> if value present as an override, <code>false</code> if not
     */
    @Override
    public boolean containsValue(final Object value) {
        return getmOverrides(true).containsValue(value) || this.mDefaults.containsValue(value);
    }

    /**
     * Get the set of entries.
     *
     * @return override entries
     */
    @Override
    public Set<Entry<String, Object>> entrySet() {
        Set<Entry<String, Object>> ret = new HashSet<Entry<String, Object>>(this.mDefaults.entrySet());
        ret.addAll(getmOverrides(true).entrySet());
        return ret;
    }

    static class TargetObjectTask implements GetValueTask {

        public TargetObjectTask(Object target) {
            super();
            this.target = target;
        }

        Object target;

        @Override
        public Object call(Env env) {
            return target;
        }

    }

    static interface GetValueTask {
        Object call(Env env);
    }

    /**
     * Internal variable tasks to get the value.
     */
    private static final IdentityHashMap<String, GetValueTask> INTERNAL_VARIABLES =
            new IdentityHashMap<String, GetValueTask>();

    static {
       // INTERNAL_VARIABLES.put(Constants.REDUCER_LOOP_VAR, new TargetObjectTask(Range.LOOP));
        INTERNAL_VARIABLES.put(Constants.REDUCER_EMPTY_VAR,
                new TargetObjectTask(ReducerResult.withEmpty(_Null.NIL)));
        INTERNAL_VARIABLES.put(Constants.ENV_VAR, new GetValueTask() {

            @Override
            public Object call(Env env) {
                env.instance.ensureFeatureEnabled(Feature.InternalVars);
                return env;
            }

        });
        INTERNAL_VARIABLES.put(Constants.FUNC_ARGS_VAR, new GetValueTask() {

            @Override
            public Object call(Env env) {
                env.instance.ensureFeatureEnabled(Feature.InternalVars);
                return FunctionUtils.getFunctionArguments(env);
            }

        });
        INTERNAL_VARIABLES.put(Constants.INSTANCE_VAR, new GetValueTask() {

            @Override
            public Object call(Env env) {
                env.instance.ensureFeatureEnabled(Feature.InternalVars);
                return env.instance;
            }

        });

        INTERNAL_VARIABLES.put(Constants.EXP_VAR, new GetValueTask() {

            @Override
            public Object call(Env env) {
                env.instance.ensureFeatureEnabled(Feature.InternalVars);
                return env.expression;
            }

        });

    }

    /**
     * Get value for key. If the key is present in the overrides map, the value from that map is
     * returned; otherwise, the value for the key in the defaults map is returned.
     *
     * @param key
     * @return value (<code>null</code> if key not present)
     */
    @Override
    public Object get(final Object key) {
        GetValueTask task = INTERNAL_VARIABLES.get(key);
        if (task != null) {
            return task.call(this);
        }

        Map<String, Object> overrides = getmOverrides(true);
        Object ret = null;
        if (overrides.containsKey(key)) {
            ret = overrides.get(key);
        } else {
            ret = this.mDefaults.get(key);
        }
        return ret;
    }

    /**
     * Check if no overrides are defined.
     *
     * @return <code>true</code> if no overrides, <code>false</code> if any present
     */
    @Override
    public boolean isEmpty() {
        return getmOverrides(true).isEmpty() && this.mDefaults.isEmpty();
    }

    /**
     * Get the set of keys. This only returns the keys in the overrides map.
     *
     * @return keys
     */
    @Override
    public Set<String> keySet() {
        Set<String> ret = new HashSet<String>(this.mDefaults.keySet());
        ret.addAll(getmOverrides(true).keySet());
        return ret;
    }

    /**
     * Set an override value.
     *
     * @param key
     * @param value
     * @return
     */
    public Object override(final String key, final Object value) {
        Object prior;
        Map<String, Object> overrides = getmOverrides(false);
        if (overrides.containsKey(key)) {
            prior = overrides.put(key, value);
        } else {
            overrides.put(key, value);
            prior = this.mDefaults != null ? this.mDefaults.get(key) : null;
        }
        return prior;
    }

    /**
     * Assign an value, if it's already in overrides, it will update it, otherwise set it to default
     * map.
     *
     * @param key
     * @param value
     * @return previous value for key (from default map, if not present in overrides)
     */
    @Override
    public Object put(final String key, final Object value) {
        Object prior = null;
        Map<String, Object> overrides = getmOverrides(false);
        if (overrides.containsKey(key)) {
            prior = overrides.put(key, value);
        } else {
            if (this.mDefaults.containsKey(key)) {
                prior = this.mDefaults.put(key, value);
            } else {
                overrides.put(key, value);
            }
        }
        return prior;
    }

    /**
     * Add all key-value pairs from another map into the overrides map.
     *
     * @param map
     */
    @Override
    public void putAll(final Map map) {
        getmOverrides(false).putAll(map);
    }

    /**
     * Remove a key-value pair. If the key was previously present in the overrides map it is simply
     * removed from that map. If it was not present in the overrides map but is present in the
     * defaults map, a null entry is added to the overrides map for that key.
     *
     * @param key
     * @return previous value for key
     */
    @Override
    public Object remove(final Object key) {
        if (getmOverrides(true).containsKey(key)) {
            return getmOverrides(false).remove(key);
        } else {
            return this.mDefaults.remove(key);
        }
    }

    /**
     * Remove a key-value pair from overrides.
     *
     * @param key
     * @return
     */
    public Object forgot(final Object key) {
        final Map<String, Object> overrides = getmOverrides(true);
        if (overrides != this.mDefaults && overrides.containsKey(key)) {
            return getmOverrides(false).remove(key);
        } else {
            return null;
        }
    }

    /**
     * Get the number of entries in the map. This returns the entry count for the overrides map only.
     *
     * @return entry count
     */
    @Override
    public int size() {
        return keySet().size();
    }

    /**
     * Get the values. This returns only the values in the overrides map.
     *
     * @return values
     */
    @Override
    public Collection<Object> values() {
        Collection<Object> vals = new ArrayList<Object>();
        for (String key : keySet()) {
            vals.add(get(key));
        }
        return vals;
    }

    /**
     * Gets the map as a String.
     *
     * @return a string version of the map
     */
    @Override
    public String toString() {
        return "env";
    }

    private Map<String, Object> getmOverrides(final boolean readOnly) {
        if (this.mOverrides == null) {
            if (readOnly) {
                return EMPTY_ENV;
            }
            this.mOverrides = new ArrayHashMap<>();
            // this.mOverrides = new HashMap<>();
        }
        return this.mOverrides;
    }

    /**
     * Default Value when cannot resolve class symbol.
     */
    static class NullClass {
    }
}
